/*
 * application.cpp
 *
 *  Created on: 17.01.2012
 *      Author: knz
 */

#include "application.h"

#include "app-shared.h"


Application *g_pApp = 0;

// Период опроса сокетов, [ms]
#define POLLING_PERIOD_MS 100

/*!
  Создание экзепляра приложения (singleton). Допускается один экземпляр приложения.
  Единственный конструктор класса приложения защищен - лежит в области private.
  */
Application* Application::createApplication()
{
    if (g_pApp == 0) g_pApp = new Application();

    return g_pApp;
}

/*!
  Метод, которые следует использовать для удаления приложения
  Иначе можно забыть обнулить указатель на единственный экземпляр приложения :)
  */
void Application::deleteApplication()
{
    if (g_pApp != 0) {
        delete g_pApp;
        g_pApp = 0;
    }
}

Application::Application()
{
    // Создадим экземпляр журнала
    Logger::createLogger("json-test-server", true);

    if(g_pLog) {
        g_pLog->setConsoleSwitch(g_bStderrMode);
        g_pLog->setTrimLevel(g_iVerboseLevel);
    }
    IF_NOTICE_LEVEL {
        LV_NOTICE("Application start. Verbose level %d", g_iVerboseLevel);
        LV_NOTICE("Application path: %s", g_zAppPath.data());
    }

    // Зададим названия ключей для указания параметров подключения к базе данных MySQL
    m_sMySqlHostKey     = "host";
    m_sMySqlUserKey     = "user";
    m_sMySqlPasswordKey = "password";
    m_sMySqlDbNameKey   = "dbname";

    // Зададим умолчание для параметров подключения к базе данных MySQL
    m_sMySqlHost     = "127.0.0.1";
    m_sMySqlUser     = "root";
    m_sMySqlPassword = "knzsoft";
    m_sMySqlDbName   = "test";

    // Зададим имя файла конфигурации приложения
    m_sAppConfigFile = "json-test-server.cfg";
    std::map<std::string, std::string> mpParams;
    if (readConfigFile(mpParams)) {
        processParamsMap(mpParams);
    } else {
        createDefaultConfigFile();
    }

    // Создадим экземпляр серверного сокета
    m_pServerSocket = new Socket(true);

    // Создадим экземпляр объекта управления таблицей истории в MySQL
    m_pDatabase = new AppMySql();
    m_pDatabase->connectToDatabase(m_sMySqlHost.c_str(), m_sMySqlUser.c_str(),
                                   m_sMySqlPassword.c_str(), m_sMySqlDbName.c_str());
}

Application::~Application()
{
    delete m_pServerSocket;
    delete m_pDatabase;
}

/*!
  Выполним настройку сокета сервера (bind) и передадим операционной системе
  listen-запрос по данному сокету.
  \param pchDotAddress - строка ip-адреса в точечной нотации (например "0.0.0.0")
  \param iPort - порт для подключения клиентов к серверу
  */
bool Application::serverInit(const char *pchDotAddress, int iPort)
{
    if (!m_pServerSocket->doBind(pchDotAddress, iPort)) {
        IF_ERR_LEVEL
        LG_ERR("Can't bind socket");
        return false;
    }
    if (!m_pServerSocket->doListen()) {
        IF_ERR_LEVEL
        LG_ERR("Can't do listen for socket");
        return false;
    }

    return true;
}

/*!
  Цикл приложения выполняет следующие действия в каждой итерации:

\li Проверяем подключение новых пользовательских клиентов
\li Проверяем сокеты связи с пользовательскими клиентами
\li Спим некоторое время (!!! пересмотреть перенос сна в таймаут опроса сокетов)

\return Возвращает true если работа сервера завершилась без ошибок
  */
bool Application::exec()
{
    bool bCanSleep;

    IF_DEBUG_LEVEL
    LG_DEBUG("Application loop starts ...");

    while(1) {
        if (g_saStopFlag) {
            IF_NOTICE_LEVEL
            LG_NOTICE("Server will close by system signal");
            break;
        }

        bCanSleep = true;
        // проверим данные в сокетах связи с клиентами и сокете подключения клиентов
        makeUpSockets(&bCanSleep);

        // Если нет ничего срочного, то может быть (по bCanSleep) вздремнем.
        // Для большей эффективности можно ввести больший таймаут в select() запрос
        // по обработке сокетов. Однако, так как у нас отдельные select() на подключение
        // новых клиентов и на опрос уже подключенных, то используемое решение проще.
        // !!! Обработка сокетов объединена в один select(). Пересмотреть решение!!!
        if (bCanSleep) {
            // заснем на POLLING_PERIOD_MS-ms миллисекунд
            pollingPause(POLLING_PERIOD_MS);
        }
    }

    IF_DEBUG_LEVEL
    LG_DEBUG("Application loop done");

    return true;
}

// регистрируем клиент по идентификатору
void Application::registerClientId(int id, ClientHandler* pClient)
{
    m_mpClientHandlersById[id] = pClient;
}

bool Application::sendMsg(int idSrc, int idDest, const std::string &sMsg)
{
    ClientHandler *handler = getClientHandlerById(idDest);
    if (handler == 0) return 0;

    handler->sendJsonMsg(sMsg);

    HistRow hr;
    m_pDatabase->hist_table_fill_row(hr, idSrc, idDest, sMsg.c_str());
    m_pDatabase->hist_table_insert(hr);
}

bool Application::sendHistory(ClientHandler * pDest, int idHist)
{
    // Формируем запрос по типу
    // select * from msghist where src=10 and dest=11
    std::stringstream ss;
    ss << "select * from " << m_pDatabase->histTableName() << " where ";
    ss << "src=" << pDest->id() << " and ";
    ss << "dest=" << idHist;

    std::vector<HistRow> vec;
    m_pDatabase->hist_table_select(vec, ss.str().c_str());

    std::vector<HistRow>::iterator it;
    for(it = vec.begin(); it<vec.end(); it++) {
        pDest->sendJsonHistory((*it).dest, (*it).msg);
    }
}

ClientHandler* Application::getClientHandlerById(int id)
{
    std::map<int, ClientHandler*>::iterator it;
    it = m_mpClientHandlersById.find(id);
    if (it != m_mpClientHandlersById.end()) {
        return it->second;
    }
    return 0;
}

ClientHandler* Application::getClientHandlerByIp(std::string sClientIpAddress)
{
    std::map<std::string, ClientHandler*>::iterator it;
    it = m_mpClientHandlersByIp.find(sClientIpAddress);
    if (it != m_mpClientHandlersByIp.end()) {
        return it->second;
    }
    return 0;
}

// Проверим данные в в сокете установления коннекта с клиентами и в сокетах
// связи с клиентами.
// Клиентами могут быть как программы пользовательских клиентов так и
// другие сервера, открывающие peer-соединения. Сервер не имеет
// возможности различать клиентов по типам.
void Application::makeUpSockets(bool *pbCanSleep)
{
    // Проверим готовность для серверного и клиентских сокетов
    // Набьем сокеты в m_setRead, выполним select() и вернем кол-во сокетов
    // требующих обработки.
    if (checkSocketsReadiness() == 0) return;

    // проверим серверный сокет - подключение новых клиентов
    int fd = m_pServerSocket->descriptor();
    if (FD_ISSET(fd, &m_setRead))
    {
        int iClientDescriptor;
        std::string sClientIpAddress;
        if (m_pServerSocket->doAccept(iClientDescriptor, sClientIpAddress))
        {
            // создадим информационную структуру для обработки нового клиента
            ClientHandler *handler = new ClientHandler(iClientDescriptor, sClientIpAddress);
            // запомним клиента в векторе
            m_lsClientHandlers.push_back(handler);
            m_mpClientHandlersByIp[sClientIpAddress] = handler;
        }

    }

    // проверим сокеты подключенных клиентов
    std::list<ClientHandler*>::iterator it;
    for(it=m_lsClientHandlers.begin(); it != m_lsClientHandlers.end(); ++it)
    {
        ClientHandler *handler = *it;
        if (FD_ISSET(handler->descriptor(), &m_setRead))
        {
            switch(handler->execAtom()) {
            case EC_DISCONNECT:
            case EC_ERROR:
            case EC_STOP_BY_QUERY:
            {
                m_lsClientHandlers.erase(it);
                m_mpClientHandlersByIp.erase((*it)->ipAddress());
                delete handler;
                it=m_lsClientHandlers.begin();
                int id = (*it)->id();
                if (id >= 0) m_mpClientHandlersById.erase(id);
            }
                break;
            case EC_AGAIN:
                if (pbCanSleep) *pbCanSleep = false;
                break;
            case EC_OK:
                break;
            }
        }
    }
}

/*!
  Заполним запрос к select всеми дескрипторами сокетов связи
  с пользовательскими клиентами и сокетом подключения клиентов.
  Установим таймаут ожидания в 10 мкс.

  \return Количество сокетов получивших данные для обработки
  */
int Application::checkSocketsReadiness()
{
    FD_ZERO(&m_setRead);
    int max = m_pServerSocket->descriptor();
    FD_SET(max, &m_setRead);
    std::list<ClientHandler*>::iterator it;
    for(it=m_lsClientHandlers.begin(); it != m_lsClientHandlers.end(); ++it) {
        ClientHandler *handler = *it;

        int sock = handler->descriptor();
        FD_SET(sock, &m_setRead);
        if (sock>max) max = sock;
    }
    timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 10;
    return select(max+1, &m_setRead, 0, 0, &timeout);
}

/*!
  Выполним задержку времени для данного процесса не останавливая другие процессы
  и передадим БД плагинов сервера (библиотек) информацию об очередном временном
  шаге.
  */
void Application::pollingPause(int ms)
{
    timeval timeout;
    timeout.tv_sec = ms/1000;
    timeout.tv_usec = (ms%1000)*1000;

    // выполним задержку времени для данного процесса не останавливая другие процессы
    select(0, 0, 0, 0, &timeout);
}

//! \brief Прочитать параметры конфигурации из файла в map
bool Application::readConfigFile(std::map<std::string, std::string> &mpParams)
{
    std::ifstream in(m_sAppConfigFile.c_str(), std::ios::in);
    if ( !in ) {
        IF_ERR_LEVEL
                LV_ERR("Can`t open config file: %s", m_sAppConfigFile.c_str());
        return false;
    }

    typedef enum {ST_KEY, ST_VALUE} FMS_t; // Finite Machine State Type
    FMS_t state = ST_KEY;

    std::string s_in;
    std::string s_key;

    while ( ! in.eof() ) {
        s_in.clear();
        in >> s_in; // берем строку (от пробела до пробела)

        if (s_in.empty()) continue;   // пропустим пустые строки
        if (s_in[0] == '#') continue; // пропустим комментарии

        switch (state) {
        case ST_KEY:
            s_key = s_in;
            state = ST_VALUE;
            break;
        case ST_VALUE:
            mpParams[s_key] = s_in;
            state = ST_KEY;
            break;
        }
    }
}

//! \brief Параметризовать приложение согласно данным прочитанного map
void Application::processParamsMap(std::map<std::string, std::string> &mpParams)
{
    std::string sValue = mpParams[m_sMySqlHostKey];
    if (sValue.empty()) {
        IF_WARNING_LEVEL
                LV_WARNING("Param '%s' is not defined in config file '%s'. Use default: %s",
                           m_sMySqlHostKey.c_str(), m_sAppConfigFile.c_str(),
                           m_sMySqlHost.c_str());
    } else {
        m_sMySqlHost = sValue;
    }

    sValue = mpParams[m_sMySqlUserKey];
    if (sValue.empty()) {
        IF_WARNING_LEVEL
                LV_WARNING("Param '%s' is not defined in config file '%s'. Use default: %s",
                           m_sMySqlUserKey.c_str(), m_sAppConfigFile.c_str(),
                           m_sMySqlUser.c_str());
    } else {
        m_sMySqlUser = sValue;
    }

    sValue = mpParams[m_sMySqlPasswordKey];
    if (sValue.empty()) {
        IF_WARNING_LEVEL
                LV_WARNING("Param '%s' is not defined in config file '%s'. Use default: %s",
                           m_sMySqlPasswordKey.c_str(), m_sAppConfigFile.c_str(),
                           m_sMySqlPassword.c_str());
    } else {
        m_sMySqlPassword = sValue;
    }

    sValue = mpParams[m_sMySqlDbNameKey];
    if (sValue.empty()) {
        IF_WARNING_LEVEL
                LV_WARNING("Param '%s' is not defined in config file '%s'. Use default: %s",
                           m_sMySqlDbNameKey.c_str(), m_sAppConfigFile.c_str(),
                           m_sMySqlDbName.c_str());
    } else {
        m_sMySqlDbName = sValue;
    }
}

//! \brief Создать файл конфигурации с параметрами по умолчанию
bool Application::createDefaultConfigFile()
{
    std::ofstream out(m_sAppConfigFile.c_str(), std::ios::out);
    if ( !out ) {
        IF_ERR_LEVEL
                LV_ERR("Can`t create config file: %s", m_sAppConfigFile.c_str());
        return false;
    }

    out << "# Generated by default values" << std::endl;
    out << m_sMySqlHostKey     << " " << m_sMySqlHost     << std::endl;
    out << m_sMySqlUserKey     << " " << m_sMySqlUser     << std::endl;
    out << m_sMySqlPasswordKey << " " << m_sMySqlPassword << std::endl;
    out << m_sMySqlDbNameKey   << " " << m_sMySqlDbName   << std::endl;

    IF_INFO_LEVEL
            LV_INFO("Application config file '%s' with default values was generated",
                    m_sAppConfigFile.c_str());
}

